Copyright (c) 1991-1993 Borland International, Inc. All Rights Reserved. THE dBASE LANGUAGE OBJECT EXTENSIONS ------------------------------------ Application programmers are facing increasingly complex programming requirements. For instance, building event-driven Windows applications often means learning new techniques and coordinating among several programmers writing many lines of source code. To make your job easier, Bladerunner has enhanced the dBASE language with new object oriented extensions. This document introduces the object extensions and describes some of the new techniques for using them. Another file, DBLANG.WRI, contains specifications and syntax for the new extensions. Contents -------- I. Object Terminology II. Who Needs Object Extensions? III. Object-Orientated Design in dBASE IV. Variable Scoping and Objects V. Creating Objects VI. Understanding Object Reference Variables VII. Understanding Function Pointers VIII. Creating New Classes IX. Examples Object Terminology ------------------ Before learning how object orientation applies to dBASE, you need to become familiar with certain standard terminology of object- oriented programming. The following definitions will give you an idea of how these concepts are implemented in Bladerunner. Object A collection of related memory variables. Objects consist of properties and methods. An object is an "instance" of a class. For example, a window you create, with DEFINE WINDOW, is an instance of the Window class. Class A specification, or "recipe," for a type of object. A class definition consists of two parts: first, any dBASE code required for the construction of an object, such as property assignments; second, method declarations. Bladerunner provides many pre-defined "stock" classes, such as Window and Menu. (The list of stock classes appears in the on-line help.) You can also create your own classes with the CLASS...ENDCLASS statement. Member An item contained in an object. An object's members are properties and methods. Property A memory variable contained in an object. Properties define characteristics of an object, such as size, location, or color. You can query the value of any property, and change the value of most properties. You can add new properties to an object with a simple assignment statement. Method A subroutine, such as a function or procedure, associated with an object through a function-pointer variable. Methods perform actions on an object. For example, a method can change an object's position in a window, or determine if the value of a property is valid. Some methods execute automatically when an event occurs; others execute only when called explicitly. Event An occurrence, such as a mouse click. Events cause methods to execute. For example, pressing the left mouse button while pointing to a window object executes the window's OnLeftMouseDown method. Who Needs Object Extensions? ------------------------------- Using traditional dBASE commands and techniques, you can easily build procedural user interfaces, where a series of cascading menu choices launch pre-defined procedures. However, these techniques are not well-suited for creating event-driven interfaces. In event-driven environments, like Windows, the user controls program flow. A user can click a button, activate a window, or select a menu choice at any time, and the program must respond to these events in whatever sequence they occur. To help build event-driven applications, Bladerunner provides UI language extensions (described in UI_EXTEN.TXT). Using familiar dBASE-like syntax, any dBASE programmer can define UI objects, called "controls," and activate them in one or more windows. Advanced programmers need the ability to change a control's characteristics in response to an event. For instance, after defining and displaying a window, your program might need to change the window's size, position, or color, without destroying the window and creating it again. The Bladerunner UI extensions let you change certain control characteristics using the REDEFINE commands. However, REDEFINE can't change all characteristics of a control, tell you the value of a characteristic, or add new characteristics. Developers writing sophisticated user interfaces need the ability to change, query, and even add, any characteristic to a control. The Bladerunner object extensions make this easy. Object-Oriented Design in dBASE ------------------------------- This section introduces some of the concepts behind object- oriented programming, then shows how you can apply them to dBASE. Focus on the concepts here; the details for implementing them are explained later. Three main concepts characterize an object-oriented programming environment: Encapsulation Combining data with the subroutines that operate on it to form a new structure called an object. Inheritance Defining a type, or class, of object and then using it to build a hierarchy of descendant objects, with each descendant "inheriting" the data and subroutines of its ancestors. Polymorphism Giving an object in a hierarchy the ability to implement an action that is common to all objects in the hierarchy, in whatever way is most appropriate for that object. Suppose you write programs for a small business. In one of your applications, you create a window for displaying employee records, and another for displaying customer information. For each window, you store field values to memory variables for displaying and editing. Next, suppose you want to display both windows at the same time. Doing so, you risk having employee variables conflict with variables defined for the customer information window. For instance, if you display the address for both employees and customers, you may overwrite the City, State, or Zip variables of an employee with those of a customer. To solve this, you can encapsulate the employee data, and the subroutines for manipulating it, into an Employee object. Likewise, you can encapsulate the customer data and subroutines into a Customer object. Then, instead of displaying a regular memory variable for each address item, you display the City, State, and Zip "properties" of the Employee or Customer objects. Encapsulation "hides" data in an object; so the properties of the Employee object can co-exist with the properties of the Customer object. When you encapsulate data into objects, you reduce the risk that one programmer's work will interfere with another's. Next, suppose you want to create a new window for editing prospective customers who haven't yet placed an order. You need to store the same information as a customer, plus some new fields. Instead of creating a new window from scratch, you can create a Prospect object based on the Customer object. The Prospect object inherits the characteristics of the Customer object. Inheritance is essentially programming by behavior modification. Different objects that share some common data or behavior can both inherit that information from an existing object, and then you can change only what's needed. Inheritance makes your code much easier to re-use. Next, suppose you need to display information for both full-time and part-time employees. Both employee types need to store the same information, except that part-timers don't get vacation pay. You can start by creating a PartTime object based on the Employee object. The Employee object has a subroutine, CalculatePay(), for calculating the yearly compensation. In the PartTime object, you can override CalculatePay() with another function that excludes vacation pay. This is an example of polymorphism. The dBASE SKIP command illustrates the benefits of polymorphism. SKIP works differently depending on the status of the current work area. If the work area is indexed, SKIP moves to the next record in the index; if a filter is set, SKIP moves to the next record meeting the filter condition. You don't need to tell SKIP about the work area; you just SKIP and it knows what to do. Likewise, your program can retrieve the yearly compensation of an employee the same way, by invoking CalculatePay(), regardless of the type of employee. By "hiding" the implementation of an action, you are free to change the details of the implementation without requiring changes to the interface. Suppose full-timers get a quarterly bonus; you just change CalculatePay() for the Employee object. Polymorphism enhances the reusability of your code. Variable Scoping and Objects ---------------------------- To understand how Bladerunner encapsulates variables into objects, you need to understand variable scoping. This section introduces objects as a solution to common dBASE scoping problems. The scope of a memory variable is defined by - the variable's lifespan; that is, under what circumstances a variable is released or destroyed. - the variable's visibility; that is, under what circumstances a program can access or modify a variable. Bladerunner offers two new ways to specify memory variable scopes. First, the new LOCAL command declares variables to be local to the procedure or function in which they are created. LOCAL variables are similar to PRIVATE, except that LOCAL variables are not visible in subsequently called routines. Second, the new STATIC command declares variables that are LOCAL in visibility but PUBLIC in lifespan. The following table summarizes the lifespan and visibility of dBASE variable types: Lifespan Visibility ---------------------------- ----------------------- PUBLIC Destroyed only when released Everywhere PRIVATE Destroyed when creating Creating routine, and routine ends subsequent routines LOCAL Destroyed when creating Creating routine only routine ends STATIC Destroyed only when released Creating routine only You can declare LOCAL and STATIC variables in your procedural code to protect against inadvertant overwriting of data, and increase program modularity. Also, when you create objects, variables that are members of an object are automatically LOCAL in scope. Creating objects ---------------- Create an object using the NEW operator in a memory variable assignment statement. NEW creates a new object, and creates an "object reference" variable that refers to the object. (See "Understanding Object Reference Variables" below.) The following example creates a new Window object and makes MyWin refer to it: MyWin = NEW Window() Use the member access operator (.) to refer to a member of an object. The member access operator, or "dot" operator, associates members to an object much the same way an alias operator (->) associates a field with a particular table. MyWin.Left = 200 && Move the left border to position 200. MyWin.Visible = .F. && Make MyWin invisible Objects are extensible. You can add new members to an object with a simple assignment statement. MyWin.MyPropN = 123 && Adds a new property called MyPropN MyWin.MyPropC = "yo" && Adds a new property called MyPropC Understanding Object-Reference Variables ---------------------------------------- Traditional dBASE memory variables contain values; a numeric variable contains a numeric value. In contrast, an object- reference variable contains a reference to an object, not the object itself. You create a new object-reference variable the same way you create other variables, with a simple assignment statement. To create a new object, however, use the NEW operator in an assignment statement. This example demonstrates normal dBASE memory variable assignments: X = 5 && X contains the value 5 Y = X && Y contains the value 5 X = 6 && X contains the value 6 ? X && Returns 6 ? Y && Returns 5 Now compare the previous example to the following example, using object-reference variables: MyWin = NEW Window() && Create a Window object and create && MyWin referring to the window. ? TYPE("MyWin") && Returns "O" for object reference MyWin.X = 10 && Add new property X MyWin.Y = 20 && Add new property Y YourWin = MyWin && MyWin and YourWin both refer to && the same window ? YourWin.X && Returns 10 YourWin.Y = 30 && Changes property Y to 30 ? YourWin.Y && Returns 30 ? MyWin.Y && Returns 30 YourWin = "text" && YourWin is now a normal char variable ? MyWin.Y && Still returns 30 MyWin = "text" && MyWin is now a normal char variable && The window object is destroyed The previous example demonstrates three important points about object-reference variables: - More than one object-reference variable can refer to the same object. - When you change an object, all references to the object reflect the change. - An object exists as long as an object-reference variable refers to it. When you release or re-assign all object-reference variables referring to an object, the object is destroyed. You work with object-reference variables the same way you work with other memory variables. For instance, you can pass them as parameters, return them as function results, and store them in arrays. Understanding Function Pointers ------------------------------- A method is a subroutine associated with an object through a function pointer variable, a new variable type. Function pointers refer indirectly to a function. They can be copied, passed as parameters, returned as values from functions, and used in the same way as traditional variables. Function pointers call functions indirectly using the call operator "()". For example: pFunc = Ten && Assigns Function Ten to pFunc ? pFunc() && Returns 10 Function Ten RETURN 10 Creating New Classes -------------------- You can define a new class using the CLASS...ENDCLASS statement. The following example defines a class with two properties, creates an object of that class, and queries the property values: X = NEW Numbers() && Creates a new Numbers object ? X.Ten && Returns 10 ? X.Twenty && Returns 20 CLASS Numbers MEMBER Ten, Twenty Ten = 10 Twenty = 20 ENDCLASS Using MEMBER or.This to Reference Properties The MEMBER statement in class definitions declares properties. Bladerunner also provides an alternate syntax for referencing properties in classes without explicitly declaring them. To reference properties not declared in a MEMBER statement, preface the property name with "This.". This. acts like a place holder for the object name created from the class. The following two class definitions are semantically identical: A = NEW Square2() A.Num = 5 ? A.Value() && Returns 25 CLASS Square1 MEMBER Num Num = 0 FUNCTION Value RETURN Num * Num ENDCLASS CLASS Square2 This.Num = 0 FUNCTION Value RETURN This.Num * This.Num ENDCLASS Defining and Passing Parameters You define and pass parameters for classes by placing the parameters in parentheses at the end of the class name. Parameters you pass are LOCAL in scope. The following example defines a class, Square, and passes a parameter when creating a Square object: X = NEW Square(10) && Creates a new Square object ? X.SquareNum && Returns 100 Y = NEW Square(5) && Creates a new Square object ? Y.SquareNum && Returns 25 CLASS Square(n) MEMBER Num, SquareNum Num = n SquareNum = n*n ENDCLASS Note: In this Alpha release, declaring parameters in parentheses is not implemented. You can instead declare parameters with a PARAMETERS statement as follows: CLASS SQUARE PARAMETERS n ENDCLASS Defining and calling methods You define methods for a class using a FUNCTION declaration within the class definition. Follow the same rules for declaring normal dBASE UDFs. In the previous Square example, the value of property X.SquareNum is computed when you create the object. By declaring Value as a method instead of a property, you can compute Value at any time. The following example demonstrates this: X = NEW Square2() && Create a new Square2 object X.Num = 5 && Change the value of Num ? X.Square() && Returns 25 X.Num = 6 && Change the value of Num again ? X.Square() && Returns 36 CLASS Square2 MEMBER num Num = 0 FUNCTION SquareNum RETURN Num * Num ENDCLASS Note: If you assign the same name to a method and a property, the property is assumed in expressions. Internally, Bladerunner treats methods and properties the same; a method is a property whose data type is function-pointer. This example assigns the name Ten to both a method and a property. Notice the return values: Function Ten RETURN 10 Ten = 10 pFunc = Ten && assigns value 10 to pFunc ? pFunc() && ERROR: data type mismatch A subroutine can serve as a method for more than one object. The following example demonstrates this: O = NEW Object() O.x = 10 O.addOne = FuncAddOne ? O.addOne() && Returns '11' ? O.x && Returns '11' Y = NEW Object() Y.x =30 Y.z = FuncAddOne ? Y.z() && Returns '31' FUNCTION FuncAddOne this.x = this.x + 1 RETURN this.x Examples -------- Creating a class based on another class - Inheritance You can create a class that is derived from, or "inherits," the members of another class. The parent class can be a stock class, such as Window, or a class you previously created. This technique is called sub-classing and demonstrates the concept of inheritance. Subclasses inherit all properties and methods of the parent from which they are derived. Use the OF option in the CLASS definition statement to specify the parent class from which to inherit members. The following example defines a class called Parent, and derives another class from Parent. The Derived class inherits all the members of Parent, and adds a few more. b = NEW Parent() && prints 'first ' ? b.X && prints 10 ? b.Y && prints 20 d = NEW Derived() && prints 'first second' ? d.X && prints 10 ? d.Y && prints 20 ? d.Z && prints 100 CLASS Parent MEMBER X, Y ? "first " X = 10 Y = 20 ENDCLASS CLASS Derived OF Parent MEMBER Z ?? "second" Z = 100 ENDCLASS Overriding Parent Members - Polymorphism When you derive a new class from a parent class, you can override parent members by simply re-initializing the variables or re- declaring the subroutines. The technique of overriding parent members is an example of polymorphism. Polymorphism lets you treat similar objects in a uniform fashion. This example defines a Parent class and derives a class from Parent, overriding one of Parent's methods: A = NEW Parent() B = NEW Derived() ? A.One() && Prints 'Parent 1' ? A.Two() && Prints 'Parent 2' ? B.One() && Prints 'Parent 1' ? B.Two() && Prints 'Derived 2' CLASS Parent FUNCTION One RETURN "Parent 1" FUNCTION Two RETURN "Parent 2" ENDCLASS CLASS Derived OF Parent FUNCTION Two && Override Method Two RETURN "Derived 2" ENDCLASS Arrays are Objects An array is a homogenous collection of variables; an object is a heterogeneous collection. Arrays are a kind of object. Arrays use the index operator to refer to contents. Arrays also have properties. An array has the two built-in properties, size and dimensions. Arrays are objects, so variables that refer to arrays are of type object reference. More than one variable can refer to the same array. decl a[10] ? a.size && prints 10 ? a[1] && prints false a[1] = 10 b = a && b and a refer to the same array ? b[1] && prints 10 b[1] = 20 ? a[2] && prints 20 ? a.fill("hello") ? a[4] && prints 'hello' Like all other objects, arrays are extensible. Variables can be added to arrays via assignment. For example: decl a[10] a.myprop = "hello" && add variable 'myprop' to ? a.myprop && prints "hello" Passing Properties by Reference Pass properties by reference the same way you pass other memory variables. You can also pass array elements by reference. O = NEW Object() O.X = 20 DO Func WITH O.X ? O.X && prints 30 PROCEDURE Func PARAMETERS Y Y = Y + 10 RETURN Returning Object-Reference Variables in Functions Object-reference is a valid return type for a function. For example: O = MakeObject() && Creates a new object ? O.X && Returns 10 FUNCTION MakeObject PRIVATE x x = NEW Object() x.x = 10 RETURN x && Returns an object-reference var Passing Parameters to Base Classes You can pass parameters to base classes by including them in parenthesis after the class name. For example: NOTE: The following code will not execute in this Alpha release. CLASS MyCube(n) OF MySquare(n) MEMBER CubeValue CubeValue = Num * Square ENDCLASS CLASS MySquare(n) MEMBER Num, Square Num = n Square = n*n ENDCLASS